define(['domReady', 'd3', 'jquery', 'modernizr', 'backbone', 'underscore'], function (domReady, d3, $, Modernizr, Backbone, _) {

    var Graph = function (options) {
        var width = options.width;
        var height = options.height;
        var minScale = 0.5;
        var maxScale = 8;

        _.bindAll(this, 'tickUpdate', 'getRadiusForNode');

        this.width = width;
        this.height = height;
        this.xScale = d3.scale.linear().domain([0,width]).range([0,width]);
        this.yScale = d3.scale.linear().domain([0,height]).range([0,height]);
        this.zoomRange = d3.scale.linear()
                    .domain([minScale, maxScale])
                    .rangeRound([minScale, maxScale])
                    .clamp(true)
                    .nice();
        var self = this;
        this.zoom = d3.behavior.zoom()
                            .x(this.xScale)
                            .y(this.yScale)
                            .scaleExtent([minScale, maxScale])
                            .on('zoom', function(e){ self.tickUpdate(e); });
        this.svg = d3.select('.graph')
                    .append('svg')
                    .attr('id', 'visualization')
                    .attr('width', width)
                    .attr('height', height)
                    .call(this.zoom)
                    .append('g')
                    .attr('class', 'overlay')
                    .attr('width', width)
                    .attr('height', height);
        this.url = options.url;
        this.links = undefined;
        this.nodes = undefined;
        this.labels = undefined;
        this.json = undefined;
        this.pointsQuadTree_ = undefined;
        this.labelClashTimerID_ = null;
        this.activeId_ = null;
        this.activeData_ = null;

         this.force = d3.layout.force()
            //.distance(100)
            .charge(-150)
            .size([options.width, options.height])
            .linkStrength(function (o,i) {
                // o.source o.target
                // i = link id
                return 0.5;
            })
            .linkDistance(function (o,i) {
                // o.source o.target
                // i = link id
                // console.log(o);
                return 20 + o.source.importance + o.target.importance;
            })

        var self = this;
        d3.json(this.url, function (json) {
            // Called when loaded.
            self.json = json;

            self.setupLinks()
                .setupNodes()
                .setupLabels()
                .setuptickUpdate()
                .setupClick()
                .setupHover()
                .setupDrag();

            self.force
                .nodes(self.json.nodes)
                .links(self.json.links)
                .start();
        });

    };

    _.extend(Graph.prototype, Backbone.Events);

    Graph.prototype.getRadiusForNode = function (d) {
        var importance = d.importance || 1;
        return Math.max(5, importance * 2);
    };

    Graph.prototype.setupLinks = function () {
        this.links = this.svg.selectAll('.link')
                        .data(this.json.links)
                        .enter()
                        .append('line')
                        .attr('class', 'link');
        return this;
    };

    Graph.prototype.setupNodes = function () {
        this.nodes = this.svg.selectAll('g.node')
            .data(this.json.nodes)
            .enter()
            .append('g')
            .attr('id', function (d, i) {
                return 'n' + i;
            })
            .attr('class', 'node');
        this.nodes
            .call(this.force.drag)
            .append('circle')
            .attr('r', this.getRadiusForNode)
            .attr('class', function (d) {
                var additionalClasses = d.node_style || '';
                return 'node-circle ' + additionalClasses;
            });
        /*
        this.activeNode_ = this.svg.append('g')
            .attr('id', 'hover-node')
            .attr('class', 'hidden');
        this.activeNode_.append('circle');
        */
        return this;
    };

    Graph.prototype.setupLabels = function () {
        this.labels = this.svg.selectAll('g.label')
            .data(this.json.nodes)
            .enter()
            .append('g')
            .attr('id', function (d, i) {
                return 'l' + i;
            })
            .attr('class', function (d) {
                var additionalClasses = d.label_style || '';
                return 'label ' + additionalClasses;
            });
        // TODO(ldixon): use a SVG filter to do the drop-shadow: more robust
        // and flexible approach
        this.labels
            .append('text')
            .attr('class', function (d) {
                var additionalClasses = d.label_style || '';
                return 'label-shadow ' + additionalClasses;
            })
            .attr('text-anchor', 'middle')
            .text(function (d) { return d.name; })
        this.labels
            .append('text')
            .attr('class', function (d) {
                var additionalClasses = d.label_style || '';
                return 'label-text ' + additionalClasses;
            })
            .attr('text-anchor', "middle")
            .text(function (d) { return d.name; });
        this.updateAllLabelsShowStatus();
        return this;
    };

    Graph.prototype.setuptickUpdate = function () {
        var self = this;
        this.force.on('tick', function(e){ self.tickUpdate(e); });
        return this;
    };

    Graph.prototype.setupClick = function () {
        var self = this;
        this.svg.selectAll('g.node').on('click', function (d) {
            self.trigger('click:node', d3.event, d);
        });

        this.svg.selectAll('g.label').on('click', function (d) {
            self.trigger('click:node', d3.event, d);
        });

        return this;
    };

    Graph.prototype.hover = function (d,i,hover) {
        if ((!hover && this.activeId_ != null)
            || (hover && this.activeId_ != null && this.activeId_ != i)) {
            this.activeLabel_ = d3.select('#l' + this.activeId_);
            this.activeLabel_.classed('hidden',
                this.activeData_.showStatus == 'hidden');
            this.activeLabel_.classed('hovered-label', false);
            this.activeNode_ = d3.select('#n' + this.activeId_);
            this.activeNode_.classed('hovered-node', false);
            this.activeId_ = null;
            this.activeData_ = null;
            $.each(this.json.nodes[i].links, function(i, d) {
              d3.select(d).classed('hover', false);
            });
        }
        if (hover) {
            this.activeId_ = i;
            this.activeData_ = d;
            this.activeLabel_ = d3.select('#l' + this.activeId_);
            this.activeLabel_.classed('hidden', false);
            this.activeLabel_.classed('hovered-label', true);
            this.activeNode_ = d3.select('#n' + this.activeId_);
            this.activeNode_.classed('hovered-node', true);
            $.each(this.json.nodes[i].links, function(i, d) {
              d3.select(d).classed('hover', true);
            });
        }
    };

    Graph.prototype.setupHover = function () {
        var self = this;
        var domLinks = this.svg.selectAll('.link')[0];
        // Construct mapping from nodes to links.
        $.each(this.json.links, function(i, o) {
          var addMapping = function (node, link) {
            if (undefined == node.links) {
              node.links = [];
            }
            node.links.push(domLinks[link]);
          };
          addMapping(self.json.nodes[o.source], i);
          addMapping(self.json.nodes[o.target], i);
        });
        // this.svg.selectAll('g.label')
            // .on('mouseover', function(d,i) {
                // self.hover(d3.select(self.json.nodes[i]), i, true);
            // })
            // .on('mouseout', function(d,i) {
                // self.hover(d3.select(self.json.nodes[i]), i, false);
            // });
        this.svg.selectAll('g.node')
            .on('mouseover', function(d,i) { self.hover(d,i,true); } )
            .on('mouseout', function(d,i) { self.hover(d,i,false); } );
        return this;
    };

    // Adds some special styling for dragged nodes and their labels.
    Graph.prototype.setupDrag = function () {
        var self = this;
        var drag = this.force.drag();
            listenerFactory = function (beingDragged) {
                return function (d, i) {
                    //self.hover(d,i,!beingDragged);
                    var label = d3.select('#l' + i);
                    //this.activeLabel_.classed('hidden', false);
                    label.classed('drag-label', beingDragged);
                    var node = d3.select('#n' + i);
                    label.classed('drag-node', beingDragged);
                    if (!beingDragged) {
                        self.hover(d,i,false);
                    }
                };
            };
        drag.on('dragstart.label-highlight', listenerFactory(true));
        //drag.on('drag.label-highlight', listenerFactory(true));
        drag.on('dragend.label-highlight', listenerFactory(false));
        return this;
    };

    // Given data index i, return the default visbility for it.
    Graph.prototype.getLabelDefaultShowStatus = function (i) {
        var el = d3.select('#l' + i);
        if (el.classed('always-shown-label')
            || el.classed('hovered-label')
            || el.classed('drag-label'))
            return 'visible';
        else if (el.classed('always-hidden-label'))
            return 'hidden';
        else
            return null;
    }

    Graph.prototype.resetAllLabelsShowStatus = function () {
        for (var i=0; i < this.json.nodes.length; i++) {
            this.json.nodes[i].showStatus = this.getLabelDefaultShowStatus(i);
        }
    }

    Graph.prototype.resetLabelBounds = function () {
        this.svg.selectAll('g.label')
            .each(function (d, i) {
                d.label_bounds = this.getBoundingClientRect();
            });
    }

    Graph.prototype.updateAllLabelsShowStatus = function () {
        var points = [];
        var self = this;

        clearInterval(this.labelClashTimerID_);
        this.labelClashTimerID_ = null;

        //this.resetPointsQuadTree();
        this.resetLabelBounds();
        this.resetAllLabelsShowStatus();

        // For each point without a set show status, decide if it should be
        // shown.
        for (i=0; i < this.json.nodes.length; i++) {
            var d = this.json.nodes[i];
            if (d.showStatus == null) this.setLabelShowStatus(d, i);
            //console.log("d.showStatus: " + d.showStatus);
            if (d.showStatus == 'hidden') {
                d3.select('#l' + i).classed('hidden', true);
            } else {
                d3.select('#l' + i).classed('hidden', false);
            }
        }
    };

    // Check if two boxes overlap.
    Graph.prototype.boundsOverlap = function(b1, b2) {
        return !(b1.left >= b2.right
                 || b1.top >= b2.bottom
                 || b1.right < b2.left
                 || b1.bottom < b2.top);
    }

    // i = label index = corresponding nodes position in the nodes array.
    Graph.prototype.setLabelShowStatus = function(d, i) {
        var d2;
        var j;
        for (j=0; j < this.json.nodes.length; j++) {
            d2 = this.json.nodes[j]
            if (d2.showStatus == 'visible' &&  j != i
                && this.boundsOverlap(d.label_bounds, d2.label_bounds)) {
              d.showStatus = 'hidden';
              return;
            }
        }
        if (d.showStatus == null) {
            d.showStatus = 'visible';
        }
    }

    Graph.prototype.tickUpdate = function () {
        var self = this;
        if (this.labelClashTimerID_ == null) {
            this.labelClashTimerID_ = window.setInterval(function () {
                self.updateAllLabelsShowStatus(); }, 1000);
        }
        return this;
    };

    var GeometricZoomGraph = function (options) {
        Graph.call(this, options);
    };

    GeometricZoomGraph.prototype = Object.create(Graph.prototype);

    GeometricZoomGraph.prototype.tickUpdate = function (e) {
        Graph.prototype.tickUpdate.call(this, e);

        // Apply a force to keep graph on the screen for non-square view.
        var k1 = 0.2;
        var width = this.width;
        var height = this.height;
        this.json.nodes.forEach(function (d,i) {
            //console.log(d);
            var spring_border = 20;
            if(d.x < spring_border) {
                d.x += (spring_border - d.x) * k1;
            }
            if(d.x > (width - spring_border)) {
                d.x += ((width - spring_border) - d.x) * k1;
            }
            if(d.y < spring_border) {
                d.y += (spring_border - d.y) * k1;
            }
            if(d.y > (height - spring_border)) {
                d.y += ((height - spring_border) - d.y) * k1;
            }
        });

        // restrict scrolling/panning. TODO(ldixon): Can't we use some kind of
        // translate scaling?
        var translate = this.zoom.translate();
        if (translate[0] < (- width / 2)) {
            translate[0] = (- width / 2);
        }
        if (translate[0] > (width / 2)) {
            translate[0] = width / 2;
        }
        if (translate[1] < (- height / 2)) {
            translate[1] = (- height / 2);
        }
        if (translate[1] > (height / 2)) {
            translate[1] = height / 2;
        }
        this.zoom.translate(translate);

        this.svg.attr(
            'transform',
            'translate(' + this.zoom.translate() + ')scale(' + this.zoom.scale() + ')'
        );

        this.nodes.attr(
            'transform',
            function (d) {
                return 'translate(' + d.x + ',' + d.y + ')';
            }
        );
        this.labels.attr(
            'transform',
            function (d) {
                if (d.short_description === undefined || d.short_description == '') {
                    var voffset = (d.label_bounds.bottom - d.label_bounds.top) * 0.3;
                    return 'translate(' + d.x + ',' + (d.y + voffset) + ')';
                } else {
                    var voffset = Graph.prototype.getRadiusForNode(d)
                        + (d.label_bounds.bottom - d.label_bounds.top) * 0.9;
                    return 'translate(' + d.x + ',' + (d.y + voffset) + ')';
                }
        });
        this.links
            .attr('x1', function (d) {
                return d.source.x;
            })
            .attr('y1', function (d) {
                return d.source.y;
            })
            .attr('x2', function (d) {
                return d.target.x;
            })
            .attr('y2', function (d) {
                return d.target.y;
            });

        return this;
    };

    var Popup = function (options) {
        _.bindAll(this, 'open', 'close');

        this.$el = $('.information-panel');
        this.$heading = this.$el.find('.information-heading');
        this.$shortDescription = this.$el.find('.information-short-description');
        this.$longDescription = this.$el.find('.information-long-description');
        this.$youtube = this.$el.find('.information-youtube');
        this.$link = this.$el.find('.more-information-link');
        this.$credit = this.$el.find('.information-credit');
        this.currentCoords = {
            x: undefined,
            y: undefined
        };
        this.data = {
            heading: '',
            shortDescription: '',
            longDescription: '',
            youtube: '',
            contextUrl: '',
            credit: ''
        };
        this.panelOpen = false;
        this.transitionPrefixedName = this.hyphenisor(Modernizr
            .prefixed('transition'));
        this.transformPrefixedName = this.hyphenisor(Modernizr
            .prefixed('transform'));

        this.setupCloseEvent();
    };

    _.extend(Popup.prototype, Backbone.Events);

    Popup.prototype.hyphenisor = function (str) {
        return str.replace(/([A-Z])/g, function (str, letter) {
            return '-' + letter.toLowerCase();
        })
        .replace(/^ms-/, '-ms-');
    };

    Popup.prototype.open = function (coords) {
        var css;
        var self = this;
        if (!this.panelOpen) {
            css = {
                top: coords.y + 'px',
                left: coords.x + 'px'
            };
            css[this.transitionPrefixedName] = 'none';
            this.$el.css(css);
            setTimeout(
                function () {
                    css = {
                        opacity: 1,
                        top: '',
                        left: '50%'
                    };
                    css[self.transformPrefixedName] = 'scale(1)';
                    css[self.transitionPrefixedName] = 'all 0.2s';
                    self.$el.css(css);
                },
                0
            );
            this.panelOpen = true;
        }
        this.currentCoords = coords;
    };

    Popup.prototype.close = function () {
        var css = {
            opacity: 0,
            top: this.currentCoords.y + 'px',
            left: this.currentCoords.x + 'px'
        };
        css[this.transformPrefixedName] = 'scale(0)';
        this.$el.css(css);
        this.panelOpen = false;
    };

    Popup.prototype.setupCloseEvent = function () {
        this.$el.on('click', '.close-button', this.close);
    };

    Popup.prototype.setData = function (data) {
        this.data = data;
    };

    Popup.prototype.renderContent = function () {
        this.$heading.text(this.data.heading);
        this.$shortDescription.text(this.data.shortDescription);
        this.$longDescription.text(this.data.longDescription);
        this.$youtube.html(this.data.youtube);
        this.$credit.html(this.data.credit);
        if (this.data.contextUrl) {
            this.$link.toggle(true);
            this.$link.attr('href', this.data.contextUrl);
        } else {
            this.$link.toggle(false);
            this.$link.attr('href', '');
        }
    };

    domReady(function () {
        var graph = new GeometricZoomGraph({
                width: window.innerWidth,
                height: window.innerHeight,
                url: $('body').data('graph-url')
            }),
            popup = new Popup(),
            ESCAPE_KEY_CODE = 27;

        popup.listenTo(graph, 'click:node', function (evt, obj) {
            if (obj.short_description === undefined) {
                return;
            }

            var credit = obj.credit,
                youtube_id = obj.youtube_id,
                context_url = obj.context_url;

            popup.open({
                x: evt.offsetX,
                y: evt.offsetY - 175
            });
            if (credit.indexOf("http://") === 0) {
                credit = '<a href="' +
                    credit + '" target="_blank">' + credit + '</a>';
            }
            if (credit) {
                credit = "This data entry was provided by " + credit;
            }

            if (youtube_id) {
                youtube_id =
                    '<iframe title="YouTube video player" src="http://www.youtube.com/embed/' +
                    youtube_id + '" width="480" height="390" frameborder="0"></iframe>';
                context_url = '';
            }

            popup.setData({
                heading: obj.name,
                shortDescription: obj.short_description,
                longDescription: obj.long_description,
                contextUrl: context_url,
                credit: credit,
                youtube: youtube_id
            });
            popup.renderContent();
        });

        $('body').on('click', function (evt) {
            if ($(evt.target).closest('.information-panel').length === 0
                && !$(evt.target).is('.circle')) {
                popup.close();
            }
        });
        $('body').on('keydown', function (evt) {
            if (evt.keyCode === ESCAPE_KEY_CODE) {
                popup.close();
            }
        });

        $('.zoom-in').on('click', graph.zoomIn);
        $('.zoom-out').on('click', graph.zoomOut);
    });
});
